#!/usr/bin/env python
#
# Copyright 2011 Google Inc.
# Copyright 2011 Joe LaPenna
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

"""Code necessary for tracking a pshb subscriptions.

Per:
http://blog.notdot.net/2010/02/Consuming-RSS-feeds-with-PubSubHubbub
"""

# Standard python modules
import base64
import logging
import uuid
import urllib
import urlparse

# Appengine sdk
from google.appengine.api import urlfetch
from google.appengine.ext import db
from google.appengine.ext import webapp


# Paths

SUBSCRIBE_PATH = '/task/hubbub_subscribe'
CALLBACK_PATH = '/hubbub'


# CONSTANTS
HUB_URL = 'http://pubsubhubbub.appspot.com/'


# Models

class Subscription(db.Model):
  @property
  def url(self):
    return self.key().name()

  verify_token = db.StringProperty(required=True)  # Random verification token.
  callback = db.StringProperty(required=True)  # Post callback url.


class Subscriber(db.Model):
  @property
  def subscription(self):
    return self.parent

  @property
  def address(self):
    return self.key().name()

# Handlers

class SubscribeTask(webapp.RequestHandler):
  """Subscribes to a pubsub feed.

  Subscribing is a straightforward matter of sending an HTTP POST request to the
  correct URL, as detailed in the hubbub spec.
  """

  URL_PARAM = 'url'
  CALLBACK_PARAM = 'callback'
  SUBSCRIBER_PARAM = 'subscriber'

  def post(self):
    logging.debug('SubscribeTask: %s', self.request)
    url = self.request.get(SubscribeTask.URL_PARAM)
    callback = self.request.get(SubscribeTask.CALLBACK_PARAM)
    subscriber = self.request.get(SubscribeTask.SUBSCRIBER_PARAM, None)
    self.subscribe(url, callback, subscriber)

  def subscribe(self, url, callback, subscriber):
    """Do the whole subscription thing.

    Args:
      url: str - Track URL
      callback: str - Where posts go to
      subscriber: str - Thing making the subscription request
    """
    created, subscription, subscriber = db.run_in_transaction(
        self.add_subscription,
        url,
        callback,
        subscriber,
    )

    if created:
      self.send_subscription_request(subscription)
      logging.debug('Subscription created: %s', subscription.url)
    else:
      logging.debug('Subscription updated: %s', subscription.url)

  def add_subscription(self, topic, callback, subscriber):
    created = False
    subscription = Subscription.get_by_key_name(topic)
    if not subscription:
      created = True
      subscription = Subscription(
          key_name=topic,
          verify_token=str(uuid.uuid4()),
          callback=callback)
    subscriber = Subscriber(key_name=subscriber, parent=subscription)
    db.put([subscription, subscriber])
    return created, subscription, subscriber

  def send_subscription_request(self, subscription):
    subscribe_args = {
        'hub.callback': urlparse.urljoin(self.request.url,
            subscription.callback),
        'hub.mode': 'subscribe',
        'hub.topic': subscription.url,
        'hub.verify': 'async',
        'hub.verify_token': subscription.verify_token,
    }

    headers = {}

    if False: # HUB_CREDENTIALS:
      auth_string = "Basic " + base64.b64encode("%s:%s" % HUB_CREDENTIALS)
      headers['Authorization'] = auth_string

    response = urlfetch.fetch(HUB_URL, payload=urllib.urlencode(subscribe_args),
                              method=urlfetch.POST, headers=headers)
    logging.debug('Subscription Request: %s: %s', response.status_code,
        response.content)


class CallbackHandler(webapp.RequestHandler):
  """ Handles subscribe verification and updates."""

  def get(self):
    """Handles verification events.

    The hub does this to make sure that nobody else forged the subscription
    request, and to make sure that we are operating a valid endpoint. This is
    where the verify_token parameter from above comes in: When we receive a
    subscription callback, we should check that the hub.verify_token argument the
    hub is supplying matches the one we stored when we made the request. If it
    does, we respond to the request by echoing back the 'hub.challenge' string it
    sends us, to confirm that we really want to subscribe.
    """
    if self.request.GET['hub.mode'] == 'unsubscribe':
      self.response.headers['Content-Type'] = 'text/plain'
      self.response.out.write(self.request.GET['hub.challenge'])
      return

    if self.request.GET['hub.mode'] != 'subscribe':
      self.error(400)
      return

    subscription = Subscription.get_by_key_name(self.request.GET['hub.topic'])
    if (not subscription
        or subscription.verify_token != self.request.GET['hub.verify_token']):
      self.error(400)
      return

    self.response.headers['Content-Type'] = 'text/plain'
    self.response.out.write(self.request.GET['hub.challenge'])

  def post(self):
    """Default handler for new content notifications.

    Now that the subscription process is out of the way, we can handle the
    updates themselves. For this, we'll use the Universal Feed Parser library,
    though since the hub processes and sanitizes the feed, we could just as
    easily use a standard XML parser. Since new entries are sent as a POST
    request to the same URL as the subscription callback, we add a post() method
    to our CallbackHandler:
    """
    logging.debug('Hubbub Callback: %s / %s', self.request.url,
        self.request.body)
